<?php

/*

Desktopd Picture Server
Copyright (C) 2015  Desktopd Developer(s)

This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as
published by the Free Software Foundation, either version 3 of the
License, or (at your option) any later version.

This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
GNU Affero General Public License for more details.

You should have received a copy of the GNU Affero General Public License
along with this program.  If not, see <http://www.gnu.org/licenses/>.

https://www.gnu.org/licenses/agpl.html

*/


namespace Desktopd\PictureServer;

use Desktopd\PlatformTools\Log;


class PictureDirectory implements \IteratorAggregate, \Countable {
    const SUBSTREAM_TYPE_TEXT = 1;
    const SUBSTREAM_TYPE_JSON = 2;
    
    
    protected $dir;
    protected $pathList = array();
    protected $hashIndex = array();
    protected $index;
    
    public function __construct ($dir) {
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        
        if (!is_dir($dir)) {
            throw new \RuntimeException("No such directory: $dir");
        }
        
        $this->dir = $dir;
        $index = "$dir/.index.json";
        
        if (file_exists($index)) {
            $this->index = fopen($index, 'r+b');
            if (!is_resource($this->index)) {
                throw new \RuntimeException("Could not open the index file: $index");
            }
            if (!flock($this->index, LOCK_EX | LOCK_NB)) {
                throw new \RuntimeException("Could not lock the index file: $index");
            }
            $data = json_decode(stream_get_contents($this->index), JSON_OBJECT_AS_ARRAY);
            if (
                is_array($data)
                && isset($data['pathList'], $data['hashIndex'])
                && is_array($data['pathList']) && is_array($data['hashIndex'])
            ) {
                $this->pathList = $data['pathList'];
                $this->hashIndex = $data['hashIndex'];
            }
            unset($data);
            rewind($this->index);
        } else {
            $this->index = fopen($index, 'c+b');
            if (!is_resource($this->index)) {
                throw new \RuntimeException("Could not open a new index file: $index");
            }
            
            if (!flock($this->index, LOCK_EX | LOCK_NB)) {
                throw new \RuntimeException("Could not lock the index file: $index");
            }
        }
        
    }
    
    public function saveIndex () {
        rewind($this->index);
        
        $stat = fstat($this->index);
        $block = str_repeat("\xff", $stat['blksize']);
        for ($i = 0; $i < $stat['blocks']; $i++) {
            fwrite($this->index, $block);
        }
        
        rewind($this->index);
        ftruncate($this->index, 0);
        
        rewind($this->index);
        
        $flags = 0;
        if (defined('JSON_UNESCAPED_SLASHES')) {
            $flags |= JSON_UNESCAPED_SLASHES;
        }
        
        $result = fwrite($this->index, json_encode(array(
            'pathList' => $this->pathList
            , 'hashIndex' => $this->hashIndex
        ), $flags));
        
        rewind($this->index);
        return $result;
    }
    
    public function createDataStream ($uri) {
        if (preg_match('~^.+?:(?:[^#?]*?/)*([^#?/]+)/?(?:\?[^#]*)?(?:#.*)?$~', $uri, $matches)) {
            $name = rawurldecode($matches[1]);
        } else {
            $name = 'output';
        }
        
        $hash = hash('sha256', $uri);
        $dir = "{$this->dir}/$hash";
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        
        if (!is_dir($dir)) {
            return false;
        }
        
        $fullPath = "$dir/$name";
        $stream = fopen($fullPath, 'cb');
        if (!is_resource($stream)) {
            return false;
        }
        
        if (!flock($stream, LOCK_EX | LOCK_NB)) {
            @flock($stream, LOCK_UN | LOCK_NB);
            @fclose($stream);
            return false;
        }
        
        ftruncate($stream, 0);
        
        $path = "$hash/$name";
        
        if (!in_array($path, $this->pathList, true)) {
            $this->pathList[] = $path;
            $this->hashIndex[$hash] = $name;
            $this->saveIndex();
        }
        
        return $stream;
    }
    
    public function createSubStream ($uri, $name = 'header', $type = self::SUBSTREAM_TYPE_TEXT) {
        if (false !== ($path = $this->substreamExists($uri, $name, $type))) {
            $dataPath = $this->getPath($uri);
            if (false !== $dataPath && $path === "{$this->dir}/$dataPath") {
                return false;
            }
            unset($dataPath);
        }
        unset($path);
        
        $hash = hash('sha256', $uri);
        $dir = "{$this->dir}/$hash";
        if (!is_dir($dir)) {
            @mkdir($dir, 0755, true);
        }
        
        if (!is_dir($dir)) {
            return false;
        }
        
        $name = str_replace(array('/', '\\', "\0", "\x7f"), '', "$name");
        if ('' === $name) {
            return false;
        }
        $ext = $type === self::SUBSTREAM_TYPE_JSON ? 'json' : 'txt';
        $name = "$name.$ext";
        $fullPath = "$dir/$name";
        $stream = fopen($fullPath, 'cb');
        if (!is_resource($stream)) {
            return false;
        }
        
        if (!flock($stream, LOCK_EX | LOCK_NB)) {
            @flock($stream, LOCK_UN | LOCK_NB);
            @fclose($stream);
            return false;
        }
        
        ftruncate($stream, 0);
        
        return $stream;
    }
    
    public function dataStreamExists ($uri) {
        return false !== $this->getPath($uri);
    }
    
    public function subStreamExists ($uri, $name = 'header', $type = self::SUBSTREAM_TYPE_TEXT) {
        $hash = hash('sha256', $uri);
        $dir = "{$this->dir}/$hash";
        
        if (!is_dir($dir)) {
            return false;
        }
        
        $name = str_replace(array('/', '\\', "\0", "\x7f"), '', "$name");
        if ('' === $name) {
            return false;
        }
        
        $ext = $type === self::SUBSTREAM_TYPE_JSON ? 'json' : 'txt';
        $name = "$name.$ext";
        $path = "$dir/$name";
        
        return file_exists($path) ? $path : false;
    }
    
    public function writeSubstream ($data, $uri, $name = 'header', $type = self::SUBSTREAM_TYPE_TEXT) {
        $stream = $this->createSubStream($uri, $name, $type);
        if (!is_resource($stream)) {
            return false;
        }
        
        if (false === fwrite($stream, "$data")) {
            $this->closeStream($stream);
            return false;
        }
        
        return $this->closeStream($stream);
    }
    
    public function closeStream ($stream) {
        if (!is_resource($stream)) {
            return false;
        }
        
        flock($stream, LOCK_UN);
        return fclose($stream);
    }
    
    protected function deleteIndex ($hash) {
        if (!isset($this->hashIndex[$hash])) {
            return false;
        }
        
        $path = "$hash/{$this->hashIndex[$hash]}";
        unset($this->hashIndex[$hash]);
        foreach (array_keys($this->pathList, $path, true) as $key) {
            unset($this->pathList[$key]);
        }
        
        $this->pathList = array_values($this->pathList);
        
        $this->saveIndex();
        
        return $path;
    }
    
    public function deleteData ($uri) {
        $hash = hash('sha256', $uri);
        if (false === ($path = $this->deleteIndex($hash))) {
            return true;
        }
        
        if (!file_exists($path)) {
            return true;
        }
        
        $fp = fopen($fp, 'r+b');
        rewind($fp);
        $stat = fstat($fp);
        $block = str_repeat("\xff", $stat['blksize']);
        for ($i = 0; $i < $stat['blocks']; $i++) {
            fwrite($fp, $block);
        }
        fclose($fp);
        
        return unlink($path);
    }
    
    public function deleteURI ($uri) {
        $hash = hash('sha256', $uri);
        
        return $this->deleteURIByHash($hash);
    }
    
    public function deleteURIByHash ($hash) {
        if (!isset($this->hashIndex[$hash])) {
            return true;
        }
        
        $this->deleteIndex($hash);
        $dir = "{$this->dir}/$hash";
        
        if (!is_dir($dir)) {
            return true;
        }
        
        foreach (glob("$dir/*") as $path) {
            $fp = fopen($path, 'r+b');
            rewind($fp);
            $stat = fstat($fp);
            $block = str_repeat("\xff", $stat['blksize']);
            for ($i = 0; $i < $stat['blocks']; $i++) {
                fwrite($fp, $block);
            }
            fclose($fp);
            unlink($path);
        }
        
        return rmdir($dir);
    }
    
    public function getPath ($uri) {
        $hash = hash('sha256', $uri);
        return isset($this->hashIndex[$hash]) ? "$hash/{$this->hashIndex[$hash]}" : false;
    }
    
    public function cleanOldFiles ($max) {
        if (!is_numeric($max)) {
            return false;
        }
        
        $max = max(0, min(65536, (int) $max));
        $total = count($this->pathList);
        if ($total <= $max) {
            return 0;
        }
        
        $oldCount = $total - $max;
        $pathList = array_values($this->pathList);
        for ($i = 0; $i < $oldCount; $i++) {
            if (!isset($pathList[$i])) continue;
            $hash = explode('/', $pathList[$i])[0];
            $this->deleteURIByHash($hash);
        }
        
        return $oldCount;
    }
    
    public function getIterator () {
        return new \ArrayIterator($this->pathList);
    }
    
    public function count () {
        return count($this->pathList);
    }
    
    public function __destruct () {
        if (is_resource($this->index)) {
            flock($this->index, LOCK_UN);
            fclose($this->index);
        }
    }
    
    public function __toString () {
        return "{$this->dir}";
    }
}


// vim: ts=4 et ai

